<?php
namespace Framework;

/**
 * Framework
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2018 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package	Framework
 * @author	Framework Dev Team
 * @copyright	2014-2018 British Columbia Institute of Technology (https://bcit.ca/)
 * @license	https://opensource.org/licenses/MIT	MIT License
 * @link	https://Framework.com
 * @since	Version 3.0.0
 * @filesource
 */

use Framework\Exception\MethodNotAllowedHttpException;
use Framework\Exception\NotFoundHttpException;
use Framework\Filesystem\Adapter\Local;
use Framework\Filesystem\AdapterInterface;
use Framework\Filesystem\Filesystem;
use Framework\Filesystem\FilesystemInterface;
use Framework\Route\RouteCollector;
use Framework\Route\RouteParser;
use Framework\Support\Arr;
use Framework\Support\Str;
use Framework\Support\Collection;
use Framework\Container\Container;
use Framework\Http\Request;
use Framework\Http\Response;
use Framework\Debug\Timer;
use Framework\Events\Events as Events;
/**
 * This class is the core of the framework, and will analyse the
 * request, route it to a controller, and send back the response.
 * Of course, there are variations to that flow, but this is the brains.
 */
class Application
{

    /**
     * The current version of Framework Framework
     */
    const SYS_VERSION = '1.0-dev';

    /**
     * App startup time.
     * @var mixed
     */
    protected $startTime;

    /**
     * Amount of memory at app start.
     * @var int
     */
    protected $startMemory;

    /**
     * Total app execution time
     * @var float
     */
    protected $totalTime;

    /**
     * Main application containeruration
     * @var \Framework\Container
     */
    protected $container;

    /**
     * Timer instance.
     * @var Timer
     */
    protected $timer;

    /**
     * Current request.
     * @var HTTP\Request
     */
    protected $request;

    /**
     * @var Events
     */
    protected $events;
    /**
     * Current response.
     * @var HTTP\Response
     */
    protected $response;

    /**
     * Router to use.
     * @var RouteCollector
     */
    protected $router;

    /**
     * Controller to use.
     * @var string|\Closure
     */
    protected $controller;

    /**
     * Controller method to invoke.
     * @var string
     */
    protected $method;

    /**
     * Params
     * @var array
     */
    protected $params;

    /**
     * Output handler to use.
     * @var string
     */
    protected $output;

    /**
     * Cache expiration time
     * @var int
     */
    protected static $cacheTTL = 0;

    /**
     * Request path to use.
     * @var string
     */
    protected $path;

    //--------------------------------------------------------------------

    public function __construct($config)
    {
        $this->startTime = microtime(true);
        $this->startMemory = memory_get_usage(true);
        $this->container = Container::getInstance();
        foreach ($config as $item=>$value){
            $this->container->instance($item, $value);
        }
        $this->initialize();
//        var_dump($this->container['forceGlobalSecureRequests']);die;
    }

    //--------------------------------------------------------------------

    /**
     * Handles some basic app and environment setup.
     */
    public function initialize()
    {
        // Set default timezone on the server
        date_default_timezone_set($this->container["appTimezone"] ?? 'UTC');
        $this->detectEnvironment();
        $this->registerDefaultServices();
    }

    protected function registerDefaultServices(){
        $this->container->singleton(Request::class, function () {
            return Request::createFromGlobals();
        });
        $this->container->alias(Request::class, "request");

        $this->container->singleton(RouteCollector::class);
        $this->container->alias(RouteCollector::class, "router");

        $this->container->singleton(Response::class);
        $this->container->alias(Response::class, "response");

        $this->container->singleton(Timer::class);
        $this->container->alias(Timer::class, "timer");

        $this->container->singleton(Events::class,function ($container){
            return new Events($container);
        });
        $this->container->alias(Events::class, "events");

        $this->container->singleton(FilesystemInterface::class, Filesystem::class);
        $this->container->alias(FilesystemInterface::class, 'filesystem');

        $this->container->singleton(AdapterInterface::class, function ($container){
            $adapter = new Local();
            $adapter->setRoot(ROOTPATH);
            return $adapter;
        });

        //cache
        //log
        //database

    }
    //--------------------------------------------------------------------

    /**
     * Launch the application!
     *
     * This is "the loop" if you will. The main entry point into the script
     * that gets the required class instances, fires off the filters,
     * tries to route the response, loads the controller and generally
     * makes all of the pieces work together.
     *
     * @param \Framework\Route\RouteCollector $routes
     */
    public function run(RouteCollector $routes = null)
    {

        $this->starttimer();
        $this->getRequestObject();
        $this->getResponseObject();
        $this->getEventsObject();
        $this->getRouterObject();

        $this->spoofRequestMethod();
        try
        {
            $this->handleRequest($routes);
        } catch (\Exception $e) {
            // If the route is a 'redirect' route, it throws
            // the exception with the $to as the message
            $this->response->redirect($e->getMessage(), 'auto', $e->getCode());
            $this->callExit(EXIT_SUCCESS);
        } catch (PageNotFoundException $e) {
            $this->display404errors($e);
        }
    }

    //--------------------------------------------------------------------

    /**
     * Handles the main request logic and fires the controller.
     *
     * @param \Framework\Route\RouteCollector $routes
     */
    protected function handleRequest(RouteCollector $routes = null)
    {
        $routeInfo = $this->tryToRouteIt($routes);

        // Run "before" filters
//        $filters = Services::filters();
//        $uri = $this->request->getPathInfo();
//        $filters->run($uri, 'before');
        $this->container->bind("controller", $routeInfo['1']);
        $returned = $this->container->call( $routeInfo['1'], $routeInfo['2']);
//var_dump($returned);die;
//        $returned = $this->startController();

        // Closure controller has run in startController().
//        if ( ! is_callable($this->controller))
//        {
//            $controller = $this->createController();
//
//            // Is there a "post_controller_constructor" event?
//            $this->events->fire('controller_constructor');
//
//            $returned = $this->runController($controller,$routeInfo);
//        }
//        else
//        {
//            $this->timer->stop('controller_constructor');
//            $this->timer->stop('controller');
//        }

        // Handle any redirects
//        if ($returned instanceof RedirectResponse)
//        {
//            $this->callExit(EXIT_SUCCESS);
//        }

        // If $returned is a string, then the controller output something,
        // probably a view, instead of echoing it directly. Send it along
        // so it can be used with the output.
        $this->gatherOutput($returned);

        // Run "after" filters
//        $response = $filters->run($uri, 'after');

//        if ($response instanceof Response)
//        {
//            $this->response = $response;
//        }

        // Save our current URI as the previous URI in the session
        // for safer, more accurate use with `previous_url()` helper function.
//        $this->storePreviousURL($this->request->uri ?? $uri);

//        unset($uri);

        $this->sendResponse();

        //--------------------------------------------------------------------
        // Is there a post-system event?
        //--------------------------------------------------------------------
        $this->events->fire('post_system');
//        var_dump($this->events);die;
    }

    //--------------------------------------------------------------------

    /**
     * You can load different containerurations depending on your
     * current environment. Setting the environment also influences
     * things like logging and error reporting.
     *
     * This can be set to anything, but default usage is:
     *
     *     development
     *     testing
     *     production
     */
    protected function detectEnvironment()
    {
        // Make sure ENVIRONMENT isn't already set by other means.
        if (! defined('ENVIRONMENT'))
        {
            // running under Continuous Integration server?
            if (getenv('CI') !== false)
            {
                define('ENVIRONMENT', 'testing');
            }
            else
            {
                define('ENVIRONMENT', $_SERVER['CI_ENVIRONMENT'] ?? 'production');
            }
        }
    }

    //--------------------------------------------------------------------

    /**
     * Start the timer
     *
     * The timer is used to display total script execution both in the
     * debug toolbar, and potentially on the displayed page.
     */
    protected function starttimer()
    {
        $this->startTime = microtime(true);

        $this->timer = $this->container[Timer::class];
        $this->timer->start('total_execution', $this->startTime);
        $this->timer->start('bootstrap');
    }

    //--------------------------------------------------------------------

    /**
     * Get our Request object, (either IncomingRequest or CLIRequest)
     * and set the server protocol based on the information provided
     * by the server.
     */
    protected function getRequestObject()
    {
        $this->request = $this->container[Request::class];

    }
    protected function getEventsObject()
    {
        $this->events = $this->container[Events::class];
    }
    protected function getRouterObject()
    {
        $this->router = $this->container[RouteCollector::class];
    }
    /**
     * Get our Response object, and set some default values, including
     * the HTTP protocol version and a default successful response.
     */
    protected function getResponseObject()
    {
        $this->response = $this->container[Response::class];

        $this->response->setProtocolVersion($this->request->getProtocolVersion());

        // Assume success until proven otherwise.
        $this->response->setStatusCode(200);
    }

    //--------------------------------------------------------------------

    /**
     * Force Secure Site Access? If the container value 'forceGlobalSecureRequests'
     * is true, will enforce that all requests to this site are made through
     * HTTPS. Will redirect the user to the current page with HTTPS, as well
     * as set the HTTP Strict Transport Security header for those browsers
     * that support it.
     *
     * @param int $duration  How long the Strict Transport Security
     *                       should be enforced for this URL.
     */
//    protected function forceSecureAccess($duration = 31536000)
//    {
//        if (isset($this->container['forceGlobalSecureRequests']) && $this->container['forceGlobalSecureRequests'] !== true)
//        {
//            return;
//        }
//
//        force_https($duration, $this->request, $this->response);
//    }

    //--------------------------------------------------------------------

    /**
     * Returns an array with our basic performance stats collected.
     *
     * @return array
     */
    public function getPerformanceStats()
    {
        return [
            'startTime'		 => $this->startTime,
            'totalTime'		 => $this->totalTime,
            'startMemory'	 => $this->startMemory
        ];
    }

    //--------------------------------------------------------------------

    /**
     * Replaces the memory_usage and elapsed_time tags.
     *
     * @param string $output
     *
     * @return string
     */
    public function displayPerformanceMetrics(string $output): string
    {
        $this->totalTime = $this->timer->getElapsedTime('total_execution');

        $output = str_replace('{elapsed_time}', $this->totalTime, $output);

        return $output;
    }

    //--------------------------------------------------------------------

    /**
     * Try to Route It - As it sounds like, works with the router to
     * match a route against the current URI. If the route is a
     * "redirect route", will also handle the redirect.
     *
     * @param RouteCollector|null $routes
     * @return array
     */
    protected function tryToRouteIt(RouteCollector $routes = null)
    {
        $this->timer->stop('bootstrap');
        $this->timer->start('routing');
        if (empty($routes) || ! $routes instanceof RouteCollector)
        {
            require APPPATH . 'Config/routers.php';
        }
        ob_start();
        $routeInfo = $this->router->dispatcher($this->request);
        $this->request->attributes->add($routeInfo[2]);
//        $this->controller = explode("@", $routeInfo[1])[0];
//        $this->method = explode("@", $routeInfo[1])[1];
        $this->timer->stop('routing');
        return $routeInfo;
    }


    //--------------------------------------------------------------------

    /**
     * Allows the request path to be set from outside the class,
     * instead of relying on CLIRequest or IncomingRequest for the path.
     *
     * This is primarily used by the Console.
     *
     * @param string $path
     *
     * @return $this
     */
    public function setPath(string $path)
    {
        $this->path = $path;

        return $this;
    }

    //--------------------------------------------------------------------

    /**
     * Displays a 404 Page Not Found error. If set, will try to
     * call the 404Override controller/method that was set in routing container.
     *
     * @param PageNotFoundException $e
     */
    protected function display404errors(PageNotFoundException $e)
    {
        // Is there a 404 Override available?
        if ($override = $this->router->get404Override())
        {
            if ($override instanceof \Closure)
            {
                echo $override();
            }
            else if (is_array($override))
            {
                $this->timer->start('controller');
                $this->timer->start('controller_constructor');

                $this->controller = $override[0];
                $this->method = $override[1];

                unset($override);

                $controller = $this->createController();
                $this->runController($controller);
            }

            $this->gatherOutput();
            $this->sendResponse();

            return;
        }

        // Display 404 Errors
        $this->response->setStatusCode($e->getCode());

        if (ENVIRONMENT !== 'testing')
        {
            if (ob_get_level() > 0)
            {
                ob_end_flush();
            }
        }
        else
        {
            // When testing, one is for phpunit, another is for test case.
            if (ob_get_level() > 2)
            {
                ob_end_flush();
            }
        }

        throw PageNotFoundException::forPageNotFound();
    }

    //--------------------------------------------------------------------

    /**
     * Gathers the script output from the buffer, replaces some execution
     * time tag in the output and displays the debug toolbar, if required.
     *
     * @param null $returned
     */
    protected function gatherOutput( $returned = null)
    {
        $this->output = ob_get_contents();
        ob_end_clean();

        // If the controller returned a response object,
        // we need to grab the body from it so it can
        // be added to anything else that might have been
        // echoed already.
        // We also need to save the instance locally
        // so that any status code changes, etc, take place.
        if ($returned instanceof Response)
        {
            $this->response = $returned;
            $returned = $returned->getBody();
        }

        if (is_string($returned))
        {
            $this->output .= $returned;
        }

        $this->output = $this->displayPerformanceMetrics($this->output);

        $this->response->setContent($this->output);

    }

    //--------------------------------------------------------------------

    /**
     * If we have a session object to use, store the current URI
     * as the previous URI. This is called just prior to sending the
     * response to the client, and will make it available next request.
     *
     * This helps provider safer, more reliable previous_url() detection.
     *
     * @param \Framework\HTTP\URI $uri
     */
    public function storePreviousURL($uri)
    {
        // This is mainly needed during testing...
        if (is_string($uri))
        {
            $uri = new URI($uri);
        }

        if (isset($_SESSION))
        {
            $_SESSION['_ci_previous_url'] = (string) $uri;
        }
    }

    //--------------------------------------------------------------------

    /**
     * Modifies the Request Object to use a different method if a POST
     * variable called _method is found.
     *
     */
    public function spoofRequestMethod()
    {

        // Only works with POSTED forms
        if ($this->request->getMethod() !== 'post')
            return;

        $method = $this->request->get('_method');

        if (empty($method))
            return;

        $this->request = $this->request->setMethod($method);
    }

    /**
     * Sends the output of this request back to the client.
     * This is what they've been waiting for!
     */
    protected function sendResponse()
    {
        $this->response->send();
    }

    //--------------------------------------------------------------------

    /**
     * Exits the application, setting the exit code for CLI-based applications
     * that might be watching.
     *
     * Made into a separate method so that it can be mocked during testing
     * without actually stopping script execution.
     *
     * @param $code
     */
    protected function callExit($code)
    {
        exit($code);
    }

    //--------------------------------------------------------------------
}
